package java

import (
	"fmt"
	. "org.xx/kris2/common"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"unicode"
)

/**
 * Reference:
 * https://jdbc.postgresql.org/documentation/head/index.html
 */

var (
	ENTITY_FIELDS_PLACEHOLDER *regexp.Regexp
)

func init() {
	ENTITY_FIELDS_PLACEHOLDER = regexp.MustCompile(`\{\s*(([a-zA-Z0-9_]+)\s*\.\s*)?\*\s*\}`)
}

func WriteModulesCode(rootPackage, destination, db string, dbEntities []*DbEntity, dbModules []*DbModule) {
	c := 0
	for _, module := range dbModules {
		_, err := WriteModuleCode(rootPackage, destination, db, dbEntities, module)

		if err != nil {
			PrintError("%s", err)
		} else {
			c++
		}
	}
	PrintInfo("%d module(s) generated", c)
}

func WriteModuleCode(rootPackage, destination, db string, entities []*DbEntity, module *DbModule) (srcName string, err error) {
	if module == nil {
		panic("Argument \"module\" must not be nil")
	}

	resetIndent()

	className := IdentToPascal(module.Name) + "Dao"
	// 类名的首字母必须大写。
	rcs := []rune(className)
	rcs[0] = unicode.ToUpper(rcs[0])
	className = string(rcs)

	package_ := getFullName(rootPackage, module.Package, "jdbc")
	dir := makePackageDir(destination, package_)
	srcName = filepath.Join(dir, className+".java")
	err = nil

	PrintVerbose("Generating module: %s (%s) ...", className, srcName)

	f, err := os.Create(srcName)
	if err != nil {
		return "", fmt.Errorf("Can not open file %q for write\n  %s", srcName, err)
	}

	defer f.Close()

	writeFileBriefDoc(f)

	writeCodeLine(f, "package %s;", package_)
	writeBlankLine(f)

	writeCodeLine(f, "import java.sql.Types;")
	writeBlankLine(f)
	writeCodeLine(f, "import org.springframework.beans.factory.config.ConfigurableBeanFactory;")
	writeCodeLine(f, "import org.springframework.context.annotation.Scope;")
	writeCodeLine(f, "import org.springframework.data.domain.Pageable;")
	writeCodeLine(f, "import org.springframework.stereotype.Component;")
	writeBlankLine(f)
	writeCodeLine(f, "import org.xx.armory.spring5.jdbc.JdbcDao;")
	writeBlankLine(f)
	writeBlankLine(f)

	writeCodeLine(f, "/**")
	writeCodeLine(f, " * Data access object: %s", module.Name)
	writeCodeLine(f, " * <p>%s</p>", formatLineInfoComment(&module.LineInfo))
	writeCodeLine(f, " */")
	writeCodeLine(f, "@Component(\"%s.%s\")", package_, module.Name)
	writeCodeLine(f, "@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)")
	writeCodeLine(f, "public class %s ", className)
	indent()
	indent()
	writeCodeLine(f, "extends JdbcDao {")
	unIndent()

	writeModuleConstructor(f, module, className)

	writeModeuleInitDao(f, db, module)

	err = writeCommands(f, db, rootPackage, module.Sqls, entities, module.Commands)

	writeModulePagable(f, className)

	unIndent()
	writeCodeLine(f, "}")

	return
}

func writeModuleConstructor(f *os.File, module *DbModule, className string) {
	// 输出注释。
	writeCodeLine(f, "/**")
	writeCodeLine(f, " * Consturctor of %s", className)
	writeCodeLine(f, " *")
	writeCodeLine(f, " */")

	// 输出方法头。
	writeCodeLine(f, "public %s(){", className)
	indent()

	// 输出方法结束。
	unIndent()
	writeCodeLine(f, "}")
	writeBlankLine(f)
	writeBlankLine(f)
}

func writeModeuleInitDao(f *os.File, db string, module *DbModule) {
	writeCodeLine(f, "@Override")

	// 输出方法头。
	writeCodeLine(f, "public void afterPropertiesSet()")
	indent()
	indent()
	writeCodeLine(f, "throws Exception {")
	unIndent()
	writeCodeLine(f, "super.afterPropertiesSet();")

	// https://dev.mysql.com/doc/refman/5.7/en/identifier-case-sensitivity.html
	// https://docs.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-2017
	// https://docs.microsoft.com/en-us/sql/t-sql/statements/windows-collation-name-transact-sql?view=sql-server-2017
	switch db {
	case DB_H2:
		writeCodeLine(f, "this.identifierCaseMode = JdbcDao.IdentifierCaseMode.UPPER;")
	case DB_ORACLE:
		writeCodeLine(f, "this.identifierCaseMode = JdbcDao.IdentifierCaseMode.UPPER;")
		writeCodeLine(f, "this.resultSetType = -10 /* OracleTypes.CURSOR */;")
	case DB_POSTGRES:
		writeCodeLine(f, "this.identifierCaseMode = JdbcDao.IdentifierCaseMode.LOWER;")
		writeCodeLine(f, "this.resultSetType = java.sql.Types.OTHER;")
	case DB_MYSQL, DB_SQLSERVER:
		writeCodeLine(f, "this.identifierCaseMode = JdbcDao.IdentifierCaseMode.LOWER;")
	default:
		panic(fmt.Sprintf("Argument %q must not be %v", "db", db))
	}

	// 输出方法结束。
	unIndent()
	writeCodeLine(f, "}")
	writeBlankLine(f)
	writeBlankLine(f)
}

func writeCommands(f *os.File, db, rootPackage string, sqls []*DbSql, entities []*DbEntity, commands []DbCommand) error {
	for _, command := range commands {
		if statement, ok := command.(*DbStatement); ok {
			if err := writeStatement(f, db, rootPackage, sqls, entities, statement); err != nil {
				return err
			}
		} else if procedure, ok := command.(*DbProcedure); ok {
			if err := writeProcedure(f, db, rootPackage, entities, procedure); err != nil {
				return err
			}
		}
	}

	return nil
}

func writeStatement(f *os.File, db, rootPackage string, sqls []*DbSql, entities []*DbEntity, statement *DbStatement) error {
	var err error = nil

	if statement == nil {
		panic("Argument \"statement\" must not be nil")
	}

	var resultEntity *DbEntity = nil
	if statement.EntityName != "" {
		found := false
		for _, entity := range entities {
			if entity.Name == statement.EntityName {
				resultEntity = entity
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("Cannot find result entity: %q", statement.EntityName)
		}
	}

	PrintVerbose("Generating statement: %s ...", IdentToCamel(statement.Name))

	writeCommandDoc(f, statement.Name, statement.Description, statement.Parameters, statement.ResultType)
	writeCommandDeclaration(f, rootPackage, statement.Name, statement.Parameters, statement.ResultType, resultEntity)
	indent()

	switch statement.ResultType {
	case RtEntity, RtString, RtInteger, RtDecimal, RtDouble, RtBoolean, RtDateTime, RtBinary, RtObjects:
		writeCodeLine(f, "return querySingle(")
	case RtEntities, RtStringList, RtIntegerList, RtDecimalList, RtDoubleList, RtBooleanList, RtDateTimeList, RtBinaryList, RtObjectsList:
		writeCodeLine(f, "return queryList(")
	case RtUpdatedCount:
		if statement.AutoKey != "" {
			writeCodeLine(f, "this.setAutoKey(%s);", formatStringLiteral(statement.AutoKey))
		}
		writeCodeLine(f, "return update(")
	default:
		return fmt.Errorf("Unknown result type: %v (%s)", statement.ResultType, statement.LineInfo)
	}
	indent()
	indent()

	writeModuleNewSqlAndArgs(f)

	err = writeModuleAddSqls(f, db, rootPackage, sqls, resultEntity, statement.Texts, statement.Parameters)
	if err != nil {
		return err
	}

	writeModuleAddArgs(f, db, statement.Parameters)

	if statement.ResultType != RtUpdatedCount {
		err = writeModuleEntityMapper(f, rootPackage, statement.ResultType, resultEntity)
		if err != nil {
			return err
		}
	}

	unIndent()
	unIndent()
	writeCodeLine(f, ");")

	// 输出方法结束。
	unIndent()
	writeCodeLine(f, "}")
	writeBlankLine(f)
	writeBlankLine(f)

	return nil
}

func writeProcedure(f *os.File, db, rootPackage string, entities []*DbEntity, procedure *DbProcedure) error {
	var err error = nil

	if procedure == nil {
		panic("Argument \"procedure\" must not be nil")
	}

	var resultEntity *DbEntity = nil
	if procedure.EntityName != "" {
		found := false
		for _, entity := range entities {
			if entity.Name == procedure.EntityName {
				resultEntity = entity
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("Cannot find result entity: %q", procedure.EntityName)
		}
	}

	PrintVerbose("Generating procedure: %s ...", IdentToCamel(procedure.Name))

	writeCommandDoc(f, procedure.Name, procedure.Description, procedure.Parameters, procedure.ResultType)
	writeCommandDeclaration(f, rootPackage, procedure.Name, procedure.Parameters, procedure.ResultType, resultEntity)
	indent()

	switch procedure.ResultType {
	case RtEntities:
		writeCodeLine(f, "return callAndQueryList(")
	case RtUpdatedCount:
		// 调用存储过程时无法获取受影响记录数，所以看作无返回值。
		writeCodeLine(f, "return call(")
	default:
		writeCodeLine(f, "return callAndQuerySingle(")
	}
	indent()
	indent()

	writeModuleNewSqlAndArgs(f)

	err = writeModuleCallProcedure(f, db, procedure.Text, procedure.Parameters, procedure.ResultType != RtUpdatedCount)
	if err != nil {
		return err
	}

	writeModuleAddPositions(f, db, procedure.Parameters)

	if procedure.ResultType != RtUpdatedCount {
		err = writeModuleEntityMapper(f, rootPackage, procedure.ResultType, resultEntity)
		if err != nil {
			return err
		}
	}

	unIndent()
	unIndent()
	writeCodeLine(f, ");")

	// 输出方法结束。
	unIndent()
	writeCodeLine(f, "}")
	writeBlankLine(f)
	writeBlankLine(f)

	return nil
}

func writeCommandDoc(f *os.File, name, description string, parameters []*DbParameter, resultType DbResultType) {
	writeCodeLine(f, "/**")
	writeCodeLine(f, " * %s", formatDescritpionComment(name, description))
	writeCodeLine(f, " *")
	for _, parameter := range parameters {
		writeCodeLine(f, " * @param %s", IdentToCamel(parameter.Name))
		writeCodeLine(f, " *        %s", formatDescritpionComment(parameter.Name, parameter.Description))
	}
	if resultType == RtUpdatedCount {
		writeCodeLine(f, " * @return Updated count")
	} else {
		writeCodeLine(f, " * @return Result of query")
	}
	writeCodeLine(f, " */")
}

func writeCommandDeclaration(f *os.File, rootPackage, name string, parameters []*DbParameter, resultType DbResultType, resultEntity *DbEntity) {
	if len(parameters) > 0 {
		writeCodeLine(f, "public %s %s(", formatResultType(resultType, rootPackage, resultEntity), IdentToCamel(name))
		indent()
		indent()
		maxIndex := len(parameters) - 1
		for index, parameter := range parameters {
			if index < maxIndex {
				writeCodeLine(f, "%s %s,", formatParameterType(parameter.Type, parameter.EnumType), IdentToCamel(parameter.Name))
			} else {
				writeCodeLine(f, "%s %s", formatParameterType(parameter.Type, parameter.EnumType), IdentToCamel(parameter.Name))
			}
		}
		unIndent()
		unIndent()
		writeCodeLine(f, ") {")
	} else {
		writeCodeLine(f, "public %s %s() {", formatResultType(resultType, rootPackage, resultEntity), IdentToCamel(name))
	}
}

func writeModuleStaticSql(f *os.File, rootPackage, sql string, resultEntity *DbEntity) error {
	for _, line := range formatSql(sql, resultEntity) {
		// 实体占位符的起始位置，例如 {*} 或者 {<TableName>.*}。
		m := ENTITY_FIELDS_PLACEHOLDER.FindStringSubmatchIndex(line)
		if m != nil {
			if resultEntity == nil {
				return fmt.Errorf("Argument %q must not be nil", "resultEntity")
			}

			// 找到了实体占位符，需要找到占位符中的实体名字。
			p1, p2, _, _, p5, p6 := m[0], m[1], m[2], m[3], m[4], m[5]

			var preSql string
			var postSql string
			var prefix string
			if p1 == 0 {
				preSql = ""
			} else {
				preSql = strings.TrimSpace(line[:p1]) // 占位符之前的SQL。
			}
			if p5 >= 0 && p6 >= 0 {
				prefix = strings.TrimSpace(line[p5:p6]) // 占位符中的前缀。
			} else {
				prefix = ""
			}
			if p2 >= len(line)-1 {
				postSql = ""
			} else {
				postSql = strings.TrimSpace(line[p2+1:]) // 占位符之后的SQL。
			}

			line = ""
			if preSql != "" {
				line += formatStringLiteral(preSql+" ") + " + "
			}
			line += fmt.Sprintf("String.join(\", \", quoteIdentifiers(%s, %s.ALL_FIELD_NAMES))", formatStringLiteral(prefix), formatFullName(getFullName(rootPackage, resultEntity.Package, "domain", resultEntity.Name)))
			if postSql != "" {
				line += " + " + formatStringLiteral(" "+postSql)
			}

			writeCodeLine(f, ".append(%s)", line)
		} else {
			writeCodeLine(f, ".append(%s)", formatStringLiteral(line))
		}
	}

	return nil
}

func writeModuleNewSqlAndArgs(f *os.File) {
	writeCodeLine(f, "new org.xx.armory.spring5.jdbc.SqlAndArgsBuilder()")
	writeCodeLine(f, "// @formatter:off")
	indent()
	indent()
}

func writeModuleAddSqls(f *os.File, db, rootPackage string, sqls []*DbSql, resultEntity *DbEntity, texts []DbText, parameters []*DbParameter) error {
	for _, text := range texts {
		if sqlRef, ok := text.(*DbSqlRef); ok {
			found := false
			for _, sql_ := range sqls {
				if sql_.Name == sqlRef.Name {
					writeModuleStaticSql(f, rootPackage, sql_.Content, resultEntity)
					found = true
					break
				}
			}
			if !found {
				return fmt.Errorf("Cannot find sql: %v", sqlRef.Name)
			}
		} else if staticText, ok := text.(*DbStaticText); ok {
			writeModuleStaticSql(f, rootPackage, staticText.Content, resultEntity)
		} else if dynamicText, ok := text.(*DbDynamicText); ok {
			writeCodeLine(f, ".dynamic(%s, %s, %s)", formatStringLiteral(dynamicText.Prepend), formatStringLiteral(dynamicText.Joint), formatStringLiteral(dynamicText.Append))
			indent()
			indent()
			for _, chunk := range dynamicText.Chunks {
				pName2 := IdentToCamel(chunk.Parameter)
				var p *DbParameter = nil
				for _, parameter := range parameters {
					if parameter.Name == chunk.Parameter {
						p = parameter
					}
				}
				if p == nil {
					return fmt.Errorf("Cannot find parameter %q", chunk.Parameter)
				}

				switch chunk.Name {
				case "isNotNull":
					writeCodeLine(f, ".addIfNotNull(%s, %s)", pName2, formatStringLiteral(chunk.Content))
				case "isNotBlank":
					writeCodeLine(f, ".addIfNotBlank(%s, %s)", pName2, formatStringLiteral(chunk.Content))
				case "isNull":
					writeCodeLine(f, ".addIfNull(%s, %s)", pName2, formatStringLiteral(chunk.Content))
				case "isBlank":
					writeCodeLine(f, ".addIfBlank(%s, %s)", pName2, formatStringLiteral(chunk.Content))
				case "equals":
					if ls, err := formatParameterValueLiteral(chunk.Value, p.Type, p.EnumType, &p.LineInfo); err != nil {
						return err
					} else {
						writeCodeLine(f, ".addIfEquals(%s, %s, %s)", pName2, ls, formatStringLiteral(chunk.Content))
					}
				case "notEquals":
					if ls, err := formatParameterValueLiteral(chunk.Value, p.Type, p.EnumType, &p.LineInfo); err != nil {
						return err
					} else {
						writeCodeLine(f, ".addIfNotEquals(%s, %s, %s)", pName2, ls, formatStringLiteral(chunk.Content))
					}
				case "in":
					if ls, err := formatParameterArrayValueLiteral(chunk.Value, p.Type, p.EnumType, &p.LineInfo); err != nil {
						return err
					} else {
						writeCodeLine(f, ".addIfIn(%s, %s, %s)", pName2, ls, formatStringLiteral(chunk.Content))
					}
				case "notIn":
					if ls, err := formatParameterArrayValueLiteral(chunk.Value, p.Type, p.EnumType, &p.LineInfo); err != nil {
						return err
					} else {
						writeCodeLine(f, ".addIfNotIn(%s, %s, %s)", pName2, ls, formatStringLiteral(chunk.Content))
					}
				default:
					return fmt.Errorf("Unknown chunk name: %v", chunk.Name)
				}
			}
			writeCodeLine(f, ".end()")
			unIndent()
			unIndent()
		}
	}

	return nil
}

func writeModuleCallProcedure(f *os.File, db, name string, parameters []*DbParameter, hasResult bool) error {
	// 根据不同的目标数据库，输出不同的语法。
	formatCallString := func(n int) string {
		if n == 0 {
			return "call " + name
		} else {
			ps := make([]string, 0, n)
			for i := 0; i < n; i++ {
				ps = append(ps, "?")
			}
			return "call " + name + "(" + strings.Join(ps, ", ") + ")"
		}
	}

	var callString string
	if hasResult {
		switch db {
		case DB_H2, DB_ORACLE:
			callString = "{" + formatCallString(len(parameters)+1) + "}"
		case DB_POSTGRES, DB_SQLSERVER:
			callString = "{? = " + formatCallString(len(parameters)) + "}"
		case DB_MYSQL:
			callString = "{" + formatCallString(len(parameters)) + "}"
		default:
			return fmt.Errorf("Unknown database: %v", db)
		}
	} else {
		switch db {
		case DB_H2, DB_ORACLE, DB_POSTGRES, DB_SQLSERVER, DB_MYSQL:
			callString = "{" + formatCallString(len(parameters)) + "}"
		default:
			return fmt.Errorf("Unknown database: %v", db)
		}
	}

	writeCodeLine(f, ".append(%s)", formatStringLiteral(callString))

	if hasResult {
		switch db {
		case DB_H2, DB_ORACLE, DB_POSTGRES, DB_SQLSERVER:
			writeCodeLine(f, ".addOutResult()")
		case DB_MYSQL:
			writeCodeLine(f, ".addReturnResult()")
		default:
			return fmt.Errorf("Unknown database: %v", db)
		}
	}

	return nil
}

func writeModuleAddArgs(f *os.File, db string, parameters []*DbParameter) {
	for _, parameter := range parameters {
		pName := parameter.Name
		pName2 := IdentToCamel(pName)

		// TODO: 此处的Sql类型，应当根据具体的目标数据库决定。
		switch parameter.Type {
		case PtString:
			if parameter.EnumType != "" {
				switch parameter.Match {
				case PmStartsWith:
					writeCodeLine(f, ".addArg(%s, this.escapeStartsWith(%s != null ? %s.stringValue() : null), %s)", formatStringLiteral(pName), pName2, pName2, "Types.VARCHAR")
				case PmEndsWith:
					writeCodeLine(f, ".addArg(%s, this.escapeEndsWith(%s != null ? %s.stringValue() : null), %s)", formatStringLiteral(pName), pName2, pName2, "Types.VARCHAR")
				case PmContains:
					writeCodeLine(f, ".addArg(%s, this.escapeContains(%s != null ? %s.stringValue() : null), %s)", formatStringLiteral(pName), pName2, pName2, "Types.VARCHAR")
				default:
					writeCodeLine(f, ".addArg(%s, %s != null ? %s.stringValue() : null, %s)", formatStringLiteral(pName), pName2, pName2, "Types.VARCHAR")
				}
			} else {
				switch parameter.Match {
				case PmStartsWith:
					writeCodeLine(f, ".addArg(%s, this.escapeStartsWith(%s), %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
				case PmEndsWith:
					writeCodeLine(f, ".addArg(%s, this.escapeEndsWith(%s), %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
				case PmContains:
					writeCodeLine(f, ".addArg(%s, this.escapeContains(%s), %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
				default:
					writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
				}
			}
		case PtInteger:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addArg(%s, %s != null ? %s.value() : null, %s)", formatStringLiteral(pName), pName2, pName2, "Types.INTEGER")
			} else {
				writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.BIGINT")
			}
		case PtDecimal:
			writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.DECIMAL")
		case PtDouble:
			writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.DOUBLE")
		case PtDateTime:
			if db == DB_POSTGRES {
				writeCodeLine(f, ".addArg(%s, org.xx.armory.commons.LocalDateUtils.buildLocalDateTime(%s), %s)", formatStringLiteral(pName), pName2, "Types.TIMESTAMP")
			} else {
				writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.TIMESTAMP")
			}
		case PtBoolean:
			writeCodeLine(f, ".addArg(%s, %s != null && %s ? 1 : 0, %s)", formatStringLiteral(pName), pName2, pName2, "Types.TINYINT")
		case PtBinary:
			writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.BINARY")
		case PtIntegerSet:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addArg(%s, this.escapeEnumSet(%s), %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
			} else {
				writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
			}
		case PtStringSet:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addArg(%s, this.escapeEnumSet(%s), %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
			} else {
				writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
			}
		default:
			writeCodeLine(f, ".addArg(%s, %s, %s)", formatStringLiteral(pName), pName2, "Types.VARCHAR")
		}
	}

	unIndent()
	unIndent()
	writeCodeLine(f, "// @formatter:on")
}

func writeModuleAddPositions(f *os.File, db string, parameters []*DbParameter) {
	for _, parameter := range parameters {
		pName := parameter.Name
		pName2 := IdentToCamel(pName)

		// TODO: 此处的Sql类型，应当根据具体的目标数据库决定。
		switch parameter.Type {
		case PtString:
			if parameter.EnumType != "" {
				switch parameter.Match {
				case PmStartsWith:
					writeCodeLine(f, ".addPositional(this.escapeStartsWith(%s != null ? %s.stringValue() : null), %s)", pName2, pName2, "Types.VARCHAR")
				case PmEndsWith:
					writeCodeLine(f, ".addPositional(this.escapeEndsWith(%s != null ? %s.stringValue() : null), %s)", pName2, pName2, "Types.VARCHAR")
				case PmContains:
					writeCodeLine(f, ".addPositional(this.escapeContains(%s != null ? %s.stringValue() : null), %s)", pName2, pName2, "Types.VARCHAR")
				default:
					writeCodeLine(f, ".addPositional(%s != null ? %s.stringValue() : null, %s)", pName2, pName2, "Types.VARCHAR")
				}
			} else {
				switch parameter.Match {
				case PmStartsWith:
					writeCodeLine(f, ".addPositional(this.escapeStartsWith(%s), %s)", pName2, "Types.VARCHAR")
				case PmEndsWith:
					writeCodeLine(f, ".addPositional(this.escapeEndsWith(%s), %s)", pName2, "Types.VARCHAR")
				case PmContains:
					writeCodeLine(f, ".addPositional(this.escapeContains(%s), %s)", pName2, "Types.VARCHAR")
				default:
					writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.VARCHAR")
				}
			}
		case PtInteger:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addPositional(%s != null ? %s.value() : null, %s)", pName2, pName2, "Types.INTEGER")
			} else {
				writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.BIGINT")
			}
		case PtDecimal:
			writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.DECIMAL")
		case PtDouble:
			writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.DOUBLE")
		case PtDateTime:
			if db == DB_POSTGRES {
				writeCodeLine(f, ".addPositional(org.xx.armory.commons.LocalDateUtils.buildLocalDateTime(%s), %s)", pName2, "Types.TIMESTAMP")
			} else {
				writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.TIMESTAMP")
			}
		case PtBoolean:
			writeCodeLine(f, ".addPositional(%s != null && %s ? 1 : 0, %s)", pName2, pName2, "Types.TINYINT")
		case PtBinary:
			writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.BINARY")
		case PtIntegerSet:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addPositional(this.escapeEnumSet(%s), %s)", pName2, "Types.VARCHAR")
			} else {
				writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.VARCHAR")
			}
		case PtStringSet:
			if parameter.EnumType != "" {
				writeCodeLine(f, ".addPositional(this.escapeEnumSet(%s), %s)", pName2, "Types.VARCHAR")
			} else {
				writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.VARCHAR")
			}
		default:
			writeCodeLine(f, ".addPositional(%s, %s)", pName2, "Types.VARCHAR")
		}
	}

	unIndent()
	unIndent()
	writeCodeLine(f, "// @formatter:on")
}

func writeModuleEntityMapper(f *os.File, rootPackage string, resultType DbResultType, resultEntity *DbEntity) error {
	switch resultType {
	case RtEntities:
		writeCodeLine(f, ", new %sMapper()", formatFullName(getFullName(rootPackage, resultEntity.Package, "jdbc", resultEntity.Name)))
	case RtEntity:
		writeCodeLine(f, ", new %sMapper()", formatFullName(getFullName(rootPackage, resultEntity.Package, "jdbc", resultEntity.Name)))
	case RtString:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.STRING_MAPPER")
	case RtInteger:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.LONG_MAPPER")
	case RtDecimal:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BIG_DECIMAL_MAPPER")
	case RtDouble:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.DOUBLE_MAPPER")
	case RtDateTime:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.DATE_MAPPER")
	case RtBoolean:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BOOLEAN_MAPPER")
	case RtBinary:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BYTES_MAPPER")
	case RtObjects:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.OBJECTS_MAPPER")
	case RtStringList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.STRING_MAPPER")
	case RtIntegerList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.LONG_MAPPER")
	case RtDecimalList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BIG_DECIMAL_MAPPER")
	case RtDoubleList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.DOUBLE_MAPPER")
	case RtDateTimeList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.DATE_MAPPER")
	case RtBooleanList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BOOLEAN_MAPPER")
	case RtBinaryList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.BYTES_MAPPER")
	case RtObjectsList:
		writeCodeLine(f, ", org.xx.armory.spring5.jdbc.ScalarMappers.OBJECTS_MAPPER")
	default:
		return fmt.Errorf("Unknown result type: %s", resultType)
	}

	return nil
}

func writeModulePagable(f *os.File, className string) {
	writeCodeLine(f, "@Override")
	writeCodeLine(f, "public %s setPageable(", className)
	indent()
	indent()
	writeCodeLine(f, "Pageable pageable")
	unIndent()
	unIndent()
	writeCodeLine(f, ") {")
	indent()
	writeCodeLine(f, "return (%s) super.setPageable(pageable);", className)
	unIndent()
	writeCodeLine(f, "}")
}
